<?php

/**
 * @name Trait
 * @author vipkwd <service@vipkwd.com>
 * @link https://github.com/wxy545812093/vipkwd-phputils
 * @license http://www.apache.org/licenses/LICENSE-2.0
 * @copyright The PHP-Tools
 */

declare(strict_types=1);

namespace Vipkwd\Utils\Libs\Crypt;

use \Exception;

trait Traits
{
    private static $_instance = [];
    private $_key; //秘钥向量
    private $_iv; //混淆向量 ->偏移量

    private function __construct(string $key, string $iv)
    {
        if (strrpos(__CLASS__, 'Aes') > 0) {
            if(isset(self::$_keyLength) && isset(self::$_mode_supports)){
                // self::$_keyLength = strlen($key);
                // if (!in_array(self::$_keyLength, array_keys(self::$_mode_supports))) {
                //     throw new Exception("Aes Key chars supports only (16|24|32) bytes:");
                // }
                self::$_modeType = self::$_mode_supports[self::$_keyLength];
                //根据输入的原始 key长度，基于sha1哈希加密给定的密码，保留字节数
                $this->_key = $this->hashKey($key,self::$_keyLength);
            }
        }
        if(!$this->_key){
            $this->_key = $this->hashKey($key,self::$_ivLength);
        }
        $this->_iv = $iv;  // ECB模式没有向量参数, CBC模式有
        if (strrpos(__CLASS__, 'AesECB') > 0) {
            $this->_iv = "";
        }

    }

    private function hashKey($key, $length){
        $key = substr(openssl_digest(openssl_digest($key, 'sha256', true), 'sha1', true), 0, $length);
        return substr(bin2hex($key), 0, $length);
    }
    /**
     * 实例化
     * 
     * @param string $key 密钥字符长度: 16|24|32, JS互通时定长：16字符
     * @param string $iv 向量 定长:16字符(des 定长8位)
     * 
     * @return AesECB|AesCBC|Des
     */
    static function instance(string $key, string $iv)
    {

        // AES-ECB 模式没有向量参数
        if (strrpos(__CLASS__, 'AesECB') === false) {
            if (strlen($iv) != self::$_ivLength) {
                throw new Exception("IV char supports only " . self::$_ivLength . " bytes:");
            }
        }

        $_k = md5($key . $iv);
        if (!isset(self::$_instance[$_k]) || !self::$_instance[$_k]) {
            self::$_instance[$_k] = new self("$key", "$iv");
        }
        return self::$_instance[$_k];
    }

    /**
     * 加密
     * @param string 要加密的字符串
     * @param boolean $trim <false> 去除base64尾部填充
     * @param boolean $urlEncode <false> "/" 转 %2F, "+" 转%2B
     * 
     * @return string 加密成功返回加密后的字符串，否则返回false
     */
    public function encrypt(string $str, bool $trim = false, bool $urlEncode = false): string
    {

        $isPadding = $this->cryptType('des') && method_exists($this, 'padding');

        $options = $isPadding ? (OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING) : OPENSSL_RAW_DATA;

        // if (strlen($str) % 4) {
        // $str = str_pad($str,strlen($str) + 4 - strlen($str) % 4, "\0");
        // }
        $str = (base64_encode($str));
        if ($isPadding) {
            // $str = $this->padding($str, 4);
        }
        $data = openssl_encrypt($str, self::$_modeType, $this->_key, $options, $this->_iv);
        if ($data === false) {
            return '';
        }

        $data = $urlEncode ? urlencode(base64_encode($data)) : base64_encode($data);
        return $trim ? rtrim(rtrim($data, "="), "=") : $data;
    }

    /**
     * 解密
     * @param string 要解密的字符串
     * 
     * @return string|null 解密成功返回加密后的字符串，否则返回 `null`
     */
    public function decrypt(string $str)
    {
        $isPadding = $this->cryptType('des') && method_exists($this, 'unPadding');

        $options = $isPadding ? (OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING) : OPENSSL_RAW_DATA;

        $str = str_replace(["%2F", "%2B"], ["/", "+"], $str);

        $_str = openssl_decrypt(base64_decode($str), self::$_modeType, $this->_key, $options, $this->_iv);
        if ($_str === false) {
            return null;
        }
        if ($isPadding) {
            // $_str = $this->unPadding($_str);
        }
        $_str = base64_decode($_str);
        try {
            $hash = json_decode($_str, true);
        } catch (Exception $e) {
            $hash = $_str;
        }
        return $hash ? $hash : $_str;
    }

    private function cryptType(string $type)
    {
        return __CLASS__ === __NAMESPACE__ . '\\' . ucfirst($type);
    }
}
